package icasue.base.lock.redis;

import icasue.base.lock.AbstractDistributedLock;
import icasue.base.lock.expecion.ServiceTimeOutException;
import icasue.base.utils.check.CommonCheck;
import icasue.base.utils.wareable.FieldConvertUtil;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.InitializingBean;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/11/3 下午3:49
 * @UpdateDate:
 * @Description:
 */
@Slf4j
@Data
@NoArgsConstructor
public class DataRedisDistributedLock extends AbstractDistributedLock implements InitializingBean {


    private MethodHandle evalHandle;
    private MethodHandle getHandle;
    private MethodHandle scriptConstructHandle;
    private MethodHandle getValueOperationsHandle;
    private Object redisTemplate;
    private Object redisConnectionFactory;
    private static final Long SUCCESS = Long.valueOf(1);
    private static final Long FAILED = Long.valueOf(0);
    private static final MethodHandles.Lookup lookup = FieldConvertUtil.methodLookup;


    public DataRedisDistributedLock(Object redisConnectionFactory){
        // notice: this template is not as a final template instance, just as a mode to clone.
        this.redisConnectionFactory = redisConnectionFactory;
    }

    public DataRedisDistributedLock(Object redisConnectionFactory,String lockPrefix,Long defaultLockTimeOutMs){
        // notice: this template is not as a final template instance, just as a mode to clone.
        super();
        this.redisConnectionFactory = redisConnectionFactory;
        if(!CommonCheck.nullStrings(lockPrefix)){
            super.CONCURRENCY_KEY_PREFIX = lockPrefix;
        }
        if(defaultLockTimeOutMs != null && defaultLockTimeOutMs.longValue() != 0L){
            super.DEFAULT_TIMEOUT = String.valueOf(defaultLockTimeOutMs);
        }
    }

    private static final String LUA_LOCK = String.join(
            Strings.EMPTY,
            "local confirmUnLockedState = redis.call('GET',KEYS[1]); " ,
            " if(confirmUnLockedState) then ",
            "    return 0; ",
            " else  " ,
            "     redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]); " +
            "     return 1;  ",
            "end;"
    );

    private static final String LUA_UNLOCK = String.join(
            Strings.EMPTY,
            "local confirmCurrentThreadLock = redis.call('GET',KEYS[1]); ",
            " if(confirmCurrentThreadLock and confirmCurrentThreadLock == ARGV[1]) then ",
            " redis.call('DEL',KEYS[1]); return 1; " ,
            " else return 0; end; "
    );

    @Override
    public String tryLock(String serviceKey) {
        if(CommonCheck.nullStrings(serviceKey)){
            throw new RuntimeException("Params 'serviceKey' in necessary when invoking DataRedisDistributedLock's method named tryLock.");
        }
        return this.tryDoLock(
                String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey),
                null,DEFAULT_TIMEOUT
        );
    }

    @Override
    public String tryLock(String serviceKey,Long timeMs) {
        if(CommonCheck.nullStrings(serviceKey) || timeMs == null || timeMs.longValue() == 0L){
            throw new RuntimeException("Params 'serviceKey', 'timeMs' is necessary when invoking DataRedisDistributedLock's method named tryLock.");
        }
        return this.tryDoLock(
                String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey),
                null, String.valueOf(timeMs)
        );
    }

    private String tryDoLock(String serviceKey,String credential,String timeMs){
        if(credential == null){
            credential = UUID.randomUUID().toString().replace("-","");
        }
        try {
            Object opsForValue = getValueOperationsHandle.bindTo(redisTemplate).invoke();
            Object locked = getHandle.bindTo(opsForValue).invoke(credential);
            if(locked != null){
                return this.tryDoLock(serviceKey,credential,timeMs);
            }else {
                Object lockLuaScript = scriptConstructHandle.invoke(LUA_LOCK, Long.class);
                Object eval = evalHandle.bindTo(redisTemplate).invoke(lockLuaScript, Arrays.asList(serviceKey), new Object[]{credential, timeMs});
                if(eval.equals(FAILED)){
                    return this.tryDoLock(serviceKey,credential,timeMs);
                }
            }
        }catch (Throwable e){
            // 这里因为是递归调用，因此一旦在树的内部抛异常，
            // 此时有三种情况: 1.application资源耗尽，2.redis资源耗尽连接超时, 3.redis服务超时异常
            // 后两种情况下在最外层finally 会抛出, 应用资源耗尽不处理。
            throw new RuntimeException("DataRedisDistributedLock :: tryDoLock occur an error when execute LOCK lua script. ",e.getCause());
        }
        return credential;
    }

    @Override
    public void unlock(String serviceKey,String credential) throws ServiceTimeOutException {
        if(CommonCheck.nullStrings(serviceKey)){
            throw new RuntimeException("Params 'serviceKey' in necessary when invoking DataRedisDistributedLock's method named unlock.");
        }else {
            serviceKey = String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey);
        }
        try {
            Object unLockLuaScript = scriptConstructHandle.invoke(LUA_UNLOCK, Long.class);
            Object eval = evalHandle.bindTo(redisTemplate).invoke(unLockLuaScript, Arrays.asList(serviceKey),new Object[]{credential});
            if(!eval.equals(SUCCESS)){
                //lock timeout or bad credential.
                // 能走到这里说明 持有的凭证确实是真实的锁，
                // 但可能因为redis 执行lua过久(网络原因)，或是redis资源耗尽(服务原因)
                // 或是业务本身执行时间过长，业务超时。因此这里抛出ServiceTimeOutException. 供用户捕获或是向外抛出 业务回滚。
                throw new ServiceTimeOutException("DataRedisDistributedLock :: unlock failed, execute service timeout or credential is illegal! " +
                        "\n Maybe redis is blocking!  serviceId: " + serviceKey);
            }
        }catch (Throwable e){
            if(e instanceof ServiceTimeOutException){
                throw (ServiceTimeOutException)e;
            }else {
                throw new RuntimeException("DataRedisDistributedLock :: unlock occur an error when execute UNLOCK lua script. ", e.getCause());
            }
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Class<?> redisTemplateMetadata = null;
        Class<?> redisConnectionFactoryMetadata = null;
        Class<?> redisAccessorMetadata = null;
        Class<?> valueOptionsMetadata = null;
        Class<?> redisOptionsMetadata = null;
        Class<?> redisScriptMetadata = null;
        Class<?> stringSerializerMetadata = null;
        Class<?> initializingBeanMetadata = null;
        try {
            // suggest data-redis 2.2.1
            redisTemplateMetadata = Class.forName("org.springframework.data.redis.core.RedisTemplate");
            redisConnectionFactoryMetadata = Class.forName("org.springframework.data.redis.connection.RedisConnectionFactory");
            redisAccessorMetadata = Class.forName("org.springframework.data.redis.core.RedisAccessor");
            valueOptionsMetadata = Class.forName("org.springframework.data.redis.core.ValueOperations");
            redisOptionsMetadata = Class.forName("org.springframework.data.redis.core.RedisOperations");
            redisScriptMetadata = Class.forName("org.springframework.data.redis.core.script.RedisScript");
            stringSerializerMetadata = Class.forName("org.springframework.data.redis.serializer.StringRedisSerializer");
            initializingBeanMetadata = Class.forName("org.springframework.beans.factory.InitializingBean");
        }catch (Throwable e){
            throw new RuntimeException("DataRedisDistributedLock :: init occur an error, cause : class metadata not found, mayBe data-redis's version is duplicate, we suggest: 2.2.1",e.getCause());
        }
        try {
            if(!CommonCheck.nullObjects(redisTemplateMetadata,valueOptionsMetadata,redisOptionsMetadata,
                    redisScriptMetadata,stringSerializerMetadata,redisConnectionFactory)) {
                if(!redisConnectionFactoryMetadata.isAssignableFrom(redisConnectionFactory.getClass())){
                    throw new RuntimeException("The instance of redisConnectionFactory which been given is not as a implement of "
                            + redisConnectionFactoryMetadata.toString());
                }
                //Clone redisTemplate, for confirm lua scriptExecutor exist and construct redisOption's valueSerializer to Long.
                Constructor<?> redisTemplateMetadataConstructor = redisTemplateMetadata.getConstructor();
                redisTemplateMetadataConstructor.setAccessible(true);
                Object genericTemplate = redisTemplateMetadataConstructor.newInstance();

                // Set redisConnectionFactory. which generic redisTemplate needed.
                Method setConnectionFactoryMethod = redisAccessorMetadata.getDeclaredMethod("setConnectionFactory",redisConnectionFactoryMetadata);
                setConnectionFactoryMethod.setAccessible(true);
                setConnectionFactoryMethod.invoke(genericTemplate,redisConnectionFactory);

                // Process post construct, from InitializingBean.
                Method afterPropertiesSetMethod = initializingBeanMetadata.getDeclaredMethod("afterPropertiesSet");
                afterPropertiesSetMethod.invoke(genericTemplate);

                // Set valueSerializer.
                Constructor<?> stringValueSerializerConstruct = stringSerializerMetadata.getConstructor();
                stringValueSerializerConstruct.setAccessible(true);
                Object stringValueSerializer = stringValueSerializerConstruct.newInstance();

                Field valueSerializerField = redisTemplateMetadata.getDeclaredField("valueSerializer");
                valueSerializerField.setAccessible(true);
                valueSerializerField.set(genericTemplate,stringValueSerializer);

                // Set template's state to init.
                Field initializedField = redisTemplateMetadata.getDeclaredField("initialized");
                initializedField.setAccessible(true);
                initializedField.set(genericTemplate,Boolean.TRUE);
                this.redisTemplate = genericTemplate;

                // Getting all invoke methods handle.
                try{
                    //Find valueOptions getting handle.
                    getValueOperationsHandle = lookup.findVirtual(redisTemplateMetadata,"opsForValue",MethodType.methodType(valueOptionsMetadata));
                    //Find redisTemplate's get method handle.
                    getHandle = lookup.findVirtual(valueOptionsMetadata,"get",MethodType.methodType(Object.class,Object.class));
                    //Find redisTemplate's eval method handle.
                    evalHandle = lookup.findVirtual(redisOptionsMetadata,"execute", MethodType.methodType(Object.class, redisScriptMetadata, List.class, Object[].class));
                    //Find DefaultRedisScript's instance handle.
                    Constructor<?> defaultRedisScriptConstructor = Class.forName("org.springframework.data.redis.core.script.DefaultRedisScript")
                            .getDeclaredConstructor(String.class, Class.class);
                    defaultRedisScriptConstructor.setAccessible(true);
                    scriptConstructHandle = lookup.unreflectConstructor(defaultRedisScriptConstructor);
                }catch (Throwable e){
                    throw new RuntimeException("DataRedisDistributedLock init error when getting methodHandles process.");
                }
            }else {
                throw new RuntimeException("DataRedisDistributedLock init need a instance of RedisTemplate. ");
            }
        }catch (Throwable e){
            throw new RuntimeException("DataRedisDistributedLock :: init occur an error, cause: mayBe reflect is relationship to data-redis's version. ",e.getCause());
        }
    }
}
